// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace System.Threading.Actors.Utilities
{
    public static class NetworkHelpers
    {
        // Read a specified number of bytes from the indicated stream.
        public static async Task ReadAllAsync(Stream source, byte[] buffer, int count)
        {
            Contract.Requires(source != null);
            Contract.Requires(buffer != null);
            Contract.Requires(count >= 0 && count <= buffer.Length);

            int bytesRead = 0;
            while (bytesRead < count)
            {
                int bytesThisTime = await source.ReadAsync(buffer, bytesRead, count - bytesRead).ConfigureAwait(continueOnCapturedContext: false);
                if (bytesThisTime <= 0)
                    throw new EndOfStreamException(String.Format("Tried to read {0} bytes, but only got {1}.  ReadAsync returned {2}", count, bytesRead, bytesThisTime));
                bytesRead += bytesThisTime;
            }
        }

        public static Encoding PreferredEncoding
        {
            get { return Encoding.UTF8; }
        }

        // Reads a buffer length, then a byte buffer, and returns the un-encoded string
        public static async Task<string> ReadStringAsync(Stream source, byte[] defaultbuffer)
        {
            Contract.Requires(source != null);
            Contract.Requires(defaultbuffer != null);
            Contract.Requires(defaultbuffer.Length >= 4);

            var tuple = await ReadRequestPacketAsync(source, defaultbuffer).ConfigureAwait(continueOnCapturedContext: false);
            return PreferredEncoding.GetString(tuple.Item1, 0, tuple.Item2);
        }

        // Encode the string, then write out a buffer length followed by a buffer
        public static void WriteString(string s, Stream target)
        {
            Contract.Requires(s != null);
            Contract.Requires(target != null);

            var buf = PreferredEncoding.GetBytes(s);
            WriteRequestPacket(target, buf, buf.Length);
        }

        // Reads an ActorFx request packet from the specified stream
        // (Request packet = 4-byte "length" integer + byte buffer.)
        // Pass in a default buffer that will *probably* suffice to hold the resultant byte 
        // buffer, but if the received length is too large, a new buffer will be created internally.
        // The eventual buffer-of-use is passed back in the Task result.
        public static async Task<Tuple<byte[], int>> ReadRequestPacketAsync(Stream source, byte[] defaultBuffer)
        {
            Contract.Requires(source != null);
            Contract.Requires(defaultBuffer != null);
            Contract.Requires(defaultBuffer.Length >= 4);

            await ReadAllAsync(source, defaultBuffer, 4).ConfigureAwait(continueOnCapturedContext: false);
            int count = BytesToInt(defaultBuffer);
            byte[] buffer = (defaultBuffer.Length < count) ? new byte[count] : defaultBuffer;
            await ReadAllAsync(source, buffer, count).ConfigureAwait(continueOnCapturedContext: false);
            return Tuple.Create(buffer, count);
        }

        // Write an ActorFx request packet to the specified stream.
        // (Request packet = 4-byte "length" integer + byte buffer.)
        public static void WriteRequestPacket(Stream target, byte[] bytes, int length)
        {
            Contract.Requires(target != null);
            Contract.Requires(bytes != null);
            Contract.Requires(length >= 0 && length <= bytes.Length);

            var lenBuf = IntToBytes(length);
            target.Write(lenBuf, 0, 4);
            target.Write(bytes, 0, length);
            target.Flush();
        }

        // Write an ActorFx request packet to the specified stream.
        // (Request packet = 4-byte "length" integer + byte buffer.)
        public static async Task WriteRequestPacketAsync(Stream target, byte[] bytes, int length)
        {
            Contract.Requires(target != null);
            Contract.Requires(bytes != null);
            Contract.Requires(length >= 0 && length <= bytes.Length);

            var lenBuf = IntToBytes(length);
            await target.WriteAsync(lenBuf, 0, 4);
            await target.WriteAsync(bytes, 0, length);
            target.Flush();
        }

        // Reads an ActorFx feedback packet (response or event) from the specified stream
        // (Feedback packet = 1-byte discriminator + 4-byte "length" integer + byte buffer.)
        // Pass in a default buffer that will *probably* suffice to hold the resultant byte 
        // buffer, but if the received length is too large, a new buffer will be created internally.
        // The eventual buffer-of-use is passed back in the Task result.
        public static async Task<Tuple<byte[], int, byte>> ReadFeedbackPacketAsync(Stream source, byte[] defaultBuffer)
        {
            Contract.Requires(source != null);
            Contract.Requires(defaultBuffer != null);
            Contract.Requires(defaultBuffer.Length >= 5);

            await ReadAllAsync(source, defaultBuffer, 5).ConfigureAwait(continueOnCapturedContext: false);
            byte discriminator = defaultBuffer[0]; // Distinguishes between request response and event publication
            int count = BytesToInt(defaultBuffer, 1);
            byte[] buffer = (defaultBuffer.Length < count) ? new byte[count] : defaultBuffer;
            await ReadAllAsync(source, buffer, count).ConfigureAwait(continueOnCapturedContext: false);
            return Tuple.Create(buffer, count, discriminator);
        }

        // Write an ActorFx request packet to the specified stream.
        // (Feedback packet = 1-byte discriminator + 4-byte "length" integer + byte buffer.)
        public static void WriteFeedbackPacket(Stream target, byte[] bytes, int length, byte discriminator)
        {
            Contract.Requires(target != null);
            Contract.Requires(bytes != null);
            Contract.Requires(length >= 0 && length <= bytes.Length);

            var lenBuf = IntToBytes(length);
            target.WriteByte(discriminator);
            target.Write(lenBuf, 0, 4);
            target.Write(bytes, 0, length);
            target.Flush();
        }

        // Explicitly convert 4 bytes to an integer.
        public static int BytesToInt(byte[] bytes, int initialOffset = 0)
        {
            Contract.Requires(bytes != null);
            Contract.Requires(initialOffset >= 0);
            Contract.Requires(bytes.Length >= initialOffset + 4);

            int result = bytes[initialOffset + 0];
            result |= (bytes[initialOffset + 1] << 8);
            result |= (bytes[initialOffset + 2] << 16);
            result |= (bytes[initialOffset + 3] << 24);

            return result;
        }

        // Explicitly convert an integer to a 4-byte buffer.
        public static byte[] IntToBytes(int value)
        {
            var bytes = new byte[4];
            IntToBytes(value, bytes);
            return bytes;
        }

        // Explicitly convert an integer to a 4-byte buffer, passing a pre-allocated byte array.
        public static void IntToBytes(int value, byte[] bytes)
        {
            Contract.Requires(bytes != null);
            Contract.Requires(bytes.Length >= 4);

            bytes[0] = (byte)value;
            bytes[1] = (byte)(value >> 8);
            bytes[2] = (byte)(value >> 16);
            bytes[3] = (byte)(value >> 24);
        }
    }
}
